Skip to content

feat(cmcdv2): implement rtp, lb, dl, pb, tpb, ttfb and ttlb#7895

Merged
robwalch merged 14 commits into
video-dev:masterfrom
qualabs:issue/cmcdv2-bitrate-keys
Jul 10, 2026
Merged

feat(cmcdv2): implement rtp, lb, dl, pb, tpb, ttfb and ttlb#7895
robwalch merged 14 commits into
video-dev:masterfrom
qualabs:issue/cmcdv2-bitrate-keys

Conversation

@cotid-qualabs

Copy link
Copy Markdown
Contributor

This PR will...

Complete the implementation of several CMCD v2 request and response keys in CMCDController that were previously missing.

  • rtp (Requested Throughput): segment bitrate × rtpSafetyFactor (default 5), rounded to nearest 100 kbps
  • lb (Lowest Bitrate): lowest bitrate within the current maxAutoLevel range
  • pb (Playhead Bitrate): bitrate of the level currently at the playhead, tracked via FRAG_CHANGED
  • tpb (Top Playhead Bitrate): bitrate of the highest level allowed by maxAutoLevel
  • dl (Deadline): buffer length divided by playback rate, rounded to nearest 100 ms
  • ttfb / ttlb: derived from fragment loader response timing by wrapping the onSuccess callback to call reporter.recordResponseReceived()

Adds a new config option rtpSafetyFactor (default: 5) to let operators tune the headroom multiplier used for rtp.

Why is this Pull Request needed?

These keys are part of the CMCD v2 spec and were previously missing. Without them, CDN servers cannot use CMCD v2 signals to make informed caching and bitrate decisions.

Are there any points in the code the reviewer needs to double check?

  • The playheadLevel tracking relies on FRAG_CHANGED — verify this is the right event to determine which level is currently at the playhead vs. being buffered ahead.
  • tpb uses hls.maxAutoLevel, which may return -1 when unconstrained; the code guards this but worth a second look.
  • The onSuccess wrapper in the fragment loader spreads callbacks — verify there are no edge cases where this wrapping could affect existing loader error handling.

Resolves issues:

#6090

Checklist

  • changes have been done against master branch, and PR does not conflict
  • new unit / functional tests have been added (whenever applicable)
  • API or design changes are documented in API.md

@cotid-qualabs cotid-qualabs marked this pull request as draft June 5, 2026 01:47
@cotid-qualabs cotid-qualabs marked this pull request as ready for review June 5, 2026 01:52
@robwalch robwalch requested a review from littlespex June 5, 2026 16:34
@robwalch robwalch added the CMCD label Jun 5, 2026
@robwalch robwalch linked an issue Jun 5, 2026 that may be closed by this pull request
littlespex
littlespex previously approved these changes Jun 5, 2026

@littlespex littlespex left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these changes look good for request/response based reports, but event based reports would not get these new values, or any values set in applyFragmentData for that matter. Ideally these fields would be set via reporter.update so that the values would persist for all reports. However, this would be a much larger change because the persistent values would need to be properly flagged with object type tokens. For now, I think this work leaves the feature in a better place than it was, so we can merge it.

@cotid-qualabs

cotid-qualabs commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

All of these changes look good for request/response based reports, but event based reports would not get these new values, or any values set in applyFragmentData for that matter. Ideally these fields would be set via reporter.update so that the values would persist for all reports. However, this would be a much larger change because the persistent values would need to be properly flagged with object type tokens. For now, I think this work leaves the feature in a better place than it was, so we can merge it.

I've been playing around with how we could workaround this and enable these keys in event mode, and here's an implementation I've been drafting for this PR:

Persists those keys in the reporter's shared state via reporter.update(), so they are automatically included in all subsequent reports — including event reports.

Each key is updated from the hls.js event that provides the most accurate context for it:

  • pb and rtp — updated on FRAG_CHANGED, which provides the fragment and its level directly. The object type token (ot) is also persisted here since fragment context is available.
  • lb and tpb — updated on LEVELS_UPDATED (initial manifest load) and MAX_AUTO_LEVEL_UPDATED (ABR cap changes), using the existing getLowestBandwidth / getTopBandwidth helpers (made optional-fragment to support this call site). These updates intentionally omit ot — without a stable fragment reference, persisting a potentially stale object type would be worse than omitting it.
  • dl — updated on BUFFER_APPENDED / BUFFER_FLUSHED alongside the existing bl update, using the current buffer length and playback rate.

The behavior of request reports is unchanged — applyFragmentData still computes all values with full per-request context, which remains the most precise snapshot at request time.

Known limitations (pre-existing or inherent to the approach):

  • tpb reflects maxAutoLevel (a bandwidth cap) rather than a true device capability cap, consistent with the existing request-report implementation.
  • dl may be slightly stale if playbackRate changes between buffer events.
  • lb/tpb event reports do not carry ot; a fully scoped solution would require per-object-type persistent state in the reporter, which is a larger refactor.

Comment thread src/controller/cmcd-controller.ts Outdated
Comment thread src/controller/cmcd-controller.ts Outdated
Comment thread src/controller/cmcd-controller.ts Outdated
Comment on lines +594 to +614
private getLowestBandwidth(fragment: Fragment | MediaFragment) {
let bitrate: number = Infinity;
let levels;
const hls = this.hls;

if (fragment.type === 'audio') {
levels = hls.audioTracks;
} else {
const max = hls.maxAutoLevel;
const len = max > -1 ? max + 1 : hls.levels.length;
levels = hls.levels.slice(0, len);
}

levels.forEach((level) => {
if (level.bitrate < bitrate) {
bitrate = level.bitrate;
}
});

return bitrate < Infinity ? bitrate : NaN;
}

@robwalch robwalch Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audio segments do not have bitrate. HLS variant ("level") bitrates are the combined bitrate of audio and video.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same goes for getTopBandwidth but it looks like I let that slip.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you wanted to use level.bitrate to calculate video bitrate, you'd need to subtract an estimate of the audio bitrate. audio bitrate can be guessed, but not measured until audio fragments are loaded.

Comment thread src/controller/cmcd-controller.ts Outdated
Comment thread tests/unit/controller/cmcd-controller.ts
Comment thread src/controller/cmcd-controller.ts Outdated
Comment thread src/controller/cmcd-controller.ts
Co-authored-by: Rob Walch <robwalch@users.noreply.github.com>
@cotid-qualabs

Copy link
Copy Markdown
Contributor Author

All comments have been addressed 😄. Let me know if there's anything else we can improve. We can also discuss the feasibility of enabling these keys for event reports on this PR

@cotid-qualabs

Copy link
Copy Markdown
Contributor Author

Following up on the known limitation around event-mode reports, I've been thinking about a scoped approach that could resolve it without the larger per-object-type refactor.

The idea is to gate all reporter.update() calls for rtp, lb, pb, tpb, and dl behind a muxed-stream check. When the active level has no separate audio group, ot=av is stable and unambiguous for the lifetime of that level and there is no risk of persisting a stale object type. In that case, we can safely include ot in every update() call, which means event-mode reports would get the full picture.

For non-muxed streams (alternate audio renditions), we'd skip the update entirely rather than report without ot or risk mixing up audio and video buffers. This sidesteps the per-object-type persistent state problem at the cost of not supporting those keys in event mode for non-muxed content , which seems like a reasonable trade-off for now.

Wanted to float this before going further, does this direction seem good to you?

@littlespex

Copy link
Copy Markdown
Collaborator

We still wouldn't want to include ot on the update calls because it doesn't have the same meaning in event reports. Instead you would use the parameterized value for the mixed buffer length, and produce values like br=(1000;av). Looking at this further, all of the buffer related fields should also be parameterized. It might be worth a separate issue to address all the parameterized fields at once (bitrate and buffer).

Comment thread src/controller/cmcd-controller.ts
Comment thread src/controller/cmcd-controller.ts
@robwalch robwalch merged commit 2dc63e7 into video-dev:master Jul 10, 2026
12 checks passed
@github-project-automation github-project-automation Bot moved this from Top priorities to Done in HLS.js Release Planning and Backlog Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

Implement CMCD requested maximum throughput (rtp) property

3 participants